/*
 * Copyright (C) 2012  Romain DUBOIS
 * 
 * This file is part of NeMoS - Network and Modular Software
 * 
 * NeMoS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * NeMoS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with NeMoS.  If not, see <http://www.gnu.org/licenses/>.
 */
package nemos.remote.util;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import nemos.remote.internal.RemoteMessage;
import nemos.util.log.NemosLogger;

import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.service.remoteserviceadmin.EndpointDescription;
import org.osgi.service.remoteserviceadmin.EndpointListener;
import org.osgi.util.tracker.ServiceTracker;

/**
 * EndpointProvider : The endpoints provider base class.
 * 
 * <p>
 * Endpoint providers can extends this class in order to delegate the endpoint
 * listeners' management and focus on the endpoints.
 * </p>
 * 
 * <p>
 * Listeners can be of an other type than {@link EndpointListener}, even if the
 * first use of this class is intented for it.
 * </p>
 * 
 * @param <T>
 *            The endpoint listener type
 * 
 * @author Romain DUBOIS
 */
@Component(componentAbstract = true)
public abstract class EndpointProvider<T> {

    /** Endpoint listeners to notify */
    private final Map<ServiceReference<T>, Collection<String>> mEndpointListeners;
    /** The {@link EndpointListener} service tracker */
    private ServiceTracker<T, T> mTracker;

    /** The active endpoints */
    private final Set<EndpointDescription> mEndpoints;
    /** The logger */
    private final NemosLogger mLogger;

    /**
     * Constructor
     */
    public EndpointProvider() {
        super();

        mEndpointListeners = Collections
                .synchronizedMap(new HashMap<ServiceReference<T>, Collection<String>>());
        mEndpoints = new HashSet<EndpointDescription>();
        mLogger = new NemosLogger(EndpointProvider.class);
    }

    /**
     * Start the component.
     * 
     * @param pContext
     *            The bundle context
     */
    @Activate
    public void activate(final BundleContext pContext) {
        mTracker = new ServiceTracker<T, T>(pContext, getListenerType(), null) {

            /**
             * {@inheritDoc}
             * 
             * @see org.osgi.util.tracker.ServiceTracker#addingService(org.osgi.framework.ServiceReference)
             */
            @Override
            public T addingService(final ServiceReference<T> pReference) {
                final T lListener = super.addingService(pReference);
                bindEndpointListener(pReference, lListener);
                return lListener;
            }

            /**
             * {@inheritDoc}
             * 
             * @see org.osgi.util.tracker.ServiceTracker#remove(org.osgi.framework.ServiceReference)
             */
            @Override
            public void remove(final ServiceReference<T> pReference) {
                super.remove(pReference);
                unbindEndpointListener(pReference);
            }

            /**
             * {@inheritDoc}
             * 
             * @see org.osgi.util.tracker.ServiceTracker#modifiedService(org.osgi.framework.ServiceReference,
             *      java.lang.Object)
             */
            @Override
            public void modifiedService(final ServiceReference<T> pReference, final T pService) {
                super.modifiedService(pReference, pService);
                bindEndpointListener(pReference, pService);
            }
        };
        mTracker.open();
    }

    /**
     * Get the endpoint listeners class.
     * 
     * @return The endpoint listeners class
     */
    protected abstract Class<T> getListenerType();

    /**
     * Stop the component.
     */
    @Deactivate
    public void deactivate() {
        mTracker.close();

        // Clean up
        synchronized (mEndpointListeners) {
            for (final EndpointDescription lEndpoint : new HashSet<EndpointDescription>(mEndpoints)) {
                notifyListeners(lEndpoint, false);
            }
            mEndpoints.clear();
        }
    }

    /**
     * Bind a endpoint listener and notify it of all matching endpoints.
     * 
     * @param pReference
     *            The listener service reference
     * @param pService
     *            The listener service
     */
    protected void bindEndpointListener(final ServiceReference<T> pReference, final T pService) {
        final Collection<String> lScope = getScope(pReference);

        if (pService != null) {

            final Map<EndpointDescription, String> lEndpoints;
            synchronized (mEndpointListeners) {
                // Collect endpoints to notify of
                lEndpoints = getMatchingEndpoints(mEndpointListeners.get(pReference), lScope);

                // Then add the listener
                mEndpointListeners.put(pReference, lScope);
            }

            // And notify it with no lock
            for (final Entry<EndpointDescription, String> lEntry : lEndpoints.entrySet()) {
                notifyEndpoint(pService, lEntry.getKey(), lEntry.getValue(), true);
            }
        }
    }

    /**
     * Get the scope of a listener.
     * 
     * @param pReference
     *            The listener service reference
     * @return The scope to use
     */
    protected abstract Collection<String> getScope(final ServiceReference<T> pReference);

    /**
     * Unbind an endpoint listnener.
     * 
     * @param pReference
     *            The listener service reference
     */
    protected void unbindEndpointListener(final ServiceReference<T> pReference) {
        mEndpointListeners.remove(pReference);
    }

    /**
     * Notify listeners of an endpoint event.
     * 
     * @param pDescription
     *            The endpoint
     * @param pAdded
     *            If the enpoint has been created or if it has been deleted
     */
    public void notifyListeners(final EndpointDescription pDescription, final boolean pAdded) {

        // Collect the matching listeners synchronuously (quick process)
        final Map<T, String> lListeners;
        synchronized (mEndpointListeners) {
            boolean lNotify = false;
            if (pAdded) {
                // If endpoint was already registered, do not notify
                lNotify = mEndpoints.add(pDescription);
            } else {
                // If endpoint was not registered, do not notify
                lNotify = mEndpoints.remove(pDescription);
            }

            if (lNotify) {
                if (pAdded) {
                    mLogger.debug(RemoteMessage.ENDPOINT_ADDED, pDescription.getId(),
                            pDescription.getFrameworkUUID(),
                            Long.valueOf(pDescription.getServiceId()), pDescription.getInterfaces());
                } else {
                    mLogger.debug(RemoteMessage.ENDPOINT_REMOVED, pDescription.getId(),
                            pDescription.getFrameworkUUID(),
                            Long.valueOf(pDescription.getServiceId()), pDescription.getInterfaces());
                }
                lListeners = getMatchingListeners(pDescription);
            } else {
                lListeners = Collections.emptyMap();
            }
        }

        // And notify them with no lock
        for (final Entry<T, String> lEntry : lListeners.entrySet()) {
            notifyEndpoint(lEntry.getKey(), pDescription, lEntry.getValue(), pAdded);
        }
    }

    /**
     * Notify a listener of an endpoint action.
     * 
     * @param pListener
     *            The listener to notify
     * @param pEndpoint
     *            The added/removed endpoint
     * @param pFilter
     *            The used who matches the endpoint
     * @param pAdded
     *            If the endpoint is added or removed
     */
    protected abstract void notifyEndpoint(final T pListener, final EndpointDescription pEndpoint,
            final String pFilter, final boolean pAdded);

    /**
     * Get listeners which scope matches the endpoint.
     * 
     * @param pDescription
     *            The endpoint description to match
     * @return The matching endpoints with one of the matching filters
     */
    private Map<T, String> getMatchingListeners(final EndpointDescription pDescription) {

        final Map<T, String> lListeners = new HashMap<T, String>();

        for (final Entry<ServiceReference<T>, Collection<String>> lEntry : mEndpointListeners
                .entrySet()) {
            for (final String lFilter : lEntry.getValue()) {
                if (pDescription.matches(lFilter)) {

                    // Retain only first matching filter
                    final T lListener = mTracker.getService(lEntry.getKey());
                    if (lListener != null) {
                        lListeners.put(lListener, lFilter);
                    }
                    break;
                }
            }
        }

        return lListeners;
    }

    /**
     * Get endpoints which match only with the new scope.
     * 
     * @param pOldScope
     *            The listener old scope
     * @param pNewScope
     *            The listener new scope
     * @return The matching endpoints with the matching filter
     */
    private Map<EndpointDescription, String> getMatchingEndpoints(
            final Collection<String> pOldScope, final Collection<String> pNewScope) {

        // Prepare old scope in a unique filter
        String lOldFilter = null;
        if (pOldScope != null && !pOldScope.isEmpty()) {
            if (pOldScope.size() == 1) {
                lOldFilter = pOldScope.iterator().next();
            } else {
                final StringBuilder lBuilder = new StringBuilder("(|"); //$NON-NLS-1$
                for (final String lFilter : pOldScope) {
                    lBuilder.append(lFilter);
                }
                lBuilder.append(')');
                lOldFilter = lBuilder.toString();
            }
        }

        // Register the listener and prepare all endpoints to notify the
        // listener of
        final Map<EndpointDescription, String> lEndpoints = new HashMap<EndpointDescription, String>();
        synchronized (mEndpointListeners) {

            for (final EndpointDescription lEndpoint : mEndpoints) {

                // Check the endpoint does not match the old scope
                if (lOldFilter != null && lEndpoint.matches(lOldFilter)) {
                    continue;
                }

                // Now check against the new scope
                for (final String lFilter : pNewScope) {
                    if (lEndpoint.matches(lFilter)) {

                        // Retain only first matching filter
                        lEndpoints.put(lEndpoint, lFilter);
                        break;
                    }
                }
            }
        }
        return lEndpoints;
    }
}
